今天一樣是接續上篇,分享幾個實際使用 MutationObserver API 的情境。
我們使用 AJSX 時,常常會需要等內容載入後,再執行一些操作。以往多使用 setInterval 或 setTimeout 來輪詢檢查內容是否已載入,現在則可以改用 MutationObserver API 來精準地監控 DOM 的變化,當內容載入完畢後,立刻觸發一個 callback。
<div id="content"></div>
// 模擬 AJAX
setTimeout(() => {
  document.getElementById('content').innerHTML = '<p>動態載入的內容</p>';
}, 2000);
// 監控 #content 的變化
const targetNode = document.getElementById('content');
const config = { childList: true };
const callback = (mutationsList, observer) => {
  for (const mutation of mutationsList) {
    if (mutation.type === 'childList') {
      console.log('載入完畢:', mutation.addedNodes[0].textContent);
    }
  }
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
我們還能使用 API 動態更新網站畫面,例如當我們在網頁中動態增加或刪除元素時,可以即時更新其他相關的元素。
⬇️ 做了一個過渡的動畫,當我新增資料時,會高亮新增的資料與增加淡入的效果
<style>
  #list {
    margin-bottom: 20px;
  }
  #itemCount {
    font-weight: bold;
    margin-bottom: 20px;
  }
  p {
    opacity: 0;
    transform: translateY(-10px);
    transition: opacity 0.5s ease, transform 0.5s ease;
  }
  p.show {
    opacity: 1;
    transform: translateY(0);
  }
  p.highlight {
    background-color: yellow;
  }
</style>
<div id="itemCount">目前有 0 筆資料</div>
<div id="list"></div>
<button id="addItem">新增資料</button>
<button id="removeItem">刪除最後一筆資料</button>
⬇️ 監聽 #list,當他的子節點改變時,MutationObserver 會觸發 updateItemCount 函式,動態更新顯示在 #itemCount 中的數量。
const list = document.getElementById('list');
const addItemButton = document.getElementById('addItem');
const removeItemButton = document.getElementById('removeItem');
const itemCountDiv = document.getElementById('itemCount');
const updateItemCount = () => {
  itemCountDiv.textContent = `目前有 ${list.children.length} 筆資料`;
};
addItemButton.addEventListener('click', () => {
  const newItem = document.createElement('p');
  newItem.textContent = `資料 ${list.children.length + 1}`;
  list.appendChild(newItem);
  // 動畫效果
  requestAnimationFrame(() => {
    newItem.classList.add('show');
    newItem.classList.add('highlight');
    setTimeout(() => {
      newItem.classList.remove('highlight');
    }, 500);
  });
});
removeItemButton.addEventListener('click', () => {
  if (list.children.length > 0) {
    list.removeChild(list.lastChild);
  }
});
// 使用 MutationObserver 監聽 DOM 變更
const observer = new MutationObserver((mutationsList) => {
  for (const mutation of mutationsList) {
    if (mutation.type === 'childList') {
      updateItemCount();
    }
  }
});
observer.observe(list, { childList: true });
⬇️ 最後是這個範例的效果圖

寫到現在,也已經介紹了不少 Web API,大家應該能發現這些 Web API 都很好用,不過要提醒大家,雖然好用,但請不要過度使用。比如 MutationObserver,他很強大沒錯,但過度使用一定會影響網站效能,畢竟是他的本質還是監聽 DOM 元素的變化。
為了避免過度監聽的狀況,我們一定要好好設定監聽的種類,就是上一章節提到的 childList、attributes 和 subtree。選對監聽的種類,避免多餘的監聽變化,可以減少不必要的消耗。
如果開發時程很趕為了快速方便,或是初學者不太清楚 DOM 元素之間的關係,很有可能把監聽的範圍設定得很大,同樣會造成無意義的監聽浪費,所以精準的設定監聽範圍,也是很重要的一件事情。
有開就要有關,當我們不再需要監聽 DOM 變化時,可以使用 disconnect 方法斷開這個監聽。
const stopObservingButton = document.getElementById('stopObserving');
stopObservingButton.addEventListener('click', () => {
    observer.disconnect();
    console.log('監聽已停止');
});
範例程式碼網址:https://mukiwu.github.io/web-api-demo/mutation.html
我還蠻喜歡用 MutationObserver API 取代 setTimeout 或 setInterval ,相較這種要一直輪詢的方式,MutationObserver API 提供了更精準的方式來主動監聽 DOM 的變化,前面給了兩個範例,包括動態新增內容、動態更新 UI,這些都能提升使用者的體驗。
未來 MutationObserver API 的應用應該會愈來愈廣泛,畢竟前端有很大一個技能就是要處理網站上的元素,所以大家可以期待更多這一類的應用。
以上有任何問題,歡迎留言討論。